BIOS Programmer's Reference

The system firmware contains a library of procedures which aims to isolate software from the specific details of the underlying hardware. They allow programs to conduct input and output to two types of devices: the user's console, and an external storage medium.

It should be remembered that the B in BIOS stands for basic; the BIOS is not a replacement for an operating system. Applications which invoke BIOS services directly will still have dependencies on the overall nature of Kestrel-2DX hardware resources. Thus, software using BIOS will only be portable between similar revisions of the Kestrel-2DX hardware.

Locating the BiosInterface Table.

Programs which are launched by the BIOS may appear anywhere in the Kestrel-2DX IPL memory space. BIOS itself, however, exists at a fixed location, starting at address 0. Thus, programs that are loaded from external media must be position independent somehow, and must find out for themselves how to access system BIOS.

The current process for locating the system BIOS is to scan memory for a well-defined, 64-bit numeric constant starting at address $10000, and ending at whatever address your program was loaded at. The following assembly language routine will perform this task for current revisions of the Kestrel-2DX hardware:

                include "biosdata.i"

                ; The first thing we need to do is find the BIOS entry points.
                ; We don't actually know where it is placed in memory, except
                ; that it will reside at an address less than _start.

_start:         auipc   s0,0                    ; S0 -> _start
                addi    sp,sp,-8
                sd      ra,0(sp)

                lui     a0,$10000               ; Start of ROM+RAM in Kestrel-2DX
                ld      a2,bs_match-_start(s0)
seek_bios:      ld      a3,bi_matchword(a0)     ; Find it yet?
                beq     a3,a2,found_bios
                addi    a0,a0,8                 ; Otherwise, try next dword in RAM
                blt     a0,s0,seek_bios

                ; If we're here, we did not find the BIOS entry point.  Wedge
                ; hard, as there's nothing else we can do.  Your code might somehow
                ; show a more helpful message on the screen, etc.

wedge:          lui     a0,$10000
                lh      a1,0(a0)
                addi    a1,a1,1
                sh      a1,0(a0)
                jal     x0,wedge

                ; If we've found the BIOS, then let's remember where we found it,
                ; then kick off the rest of our program.

found_bios:     sd      a0,bs_bi-_start(s0)
                jal     x0,start_program

                align   8
bs_match:       dword   BI_MATCHWORD
bs_bi:          dword   0

The value of BI_MATCHWORD is $0BADC0DEB105DA7A. This value is chosen to appear, probabilistically, virtually never without explicit intent within the relatively tight range of memory being scanned.

Calling BIOS Functions

Once you have a reference to the BiosInterface table, you can invoke BIOS functions using a consistent procedure:

First, load parameters into registers A0..A7.
Invoke the desired procedure through the stored pointer.

If necessary, return values will appear in either a0 and/or in a1. Alternatively, some BIOS procedures take pointers to variables, which will receive results upon successful completion of a procedure.

Here's an example where we desire to change the cursor location on the screen:

L1:         auipc   a1,0                        ; A1 -> L1
            addi    a0,a1,cursor_x - L1         ; A0 -> cursor_x
            addi    a1,a1,cursor_y - L1         ; A1 -> cursor_y
            ld      t0,bs_bi - _start(s0)       ; T0 -> BiosInterface table
            ld      t0,bi_term_cursor_swap(t0)  ; T0 -> term_cursor_swap entry point
            jalr    ra,0(t0)                    ; Call BIOS procedure

            ; ...

cursor_x:   byte    0
cursor_y:   byte    0

BIOS Function Reference

This section contains a brief summary of each of the BIOS functions currently implemented, as of ROM version 0.1.0. Each entry is formatted like so:

bios_function_name (bios_interface_offset)

results = bios_function_name(param1, param2, ...)
A0                           A0      A1      ...

Description of procedure taken when calling this function,
including results returned.

Storage Procedure Error Codes

Many of the storage-related BIOS procedures returns an error code. These procedures share the same error code space, which can be summarized as follows:

  • E_OK (0) — No errors were discovered; the SD card is safe to conduct I/O with.
  • E_UNIT (-1) — The selected device is not recognized as a valid peripheral.
  • E_CARD (-2) — The SD card has failed basic protocol checks and/or is missing.
  • E_TIMEOUT (-3) — The SD card performed valid protocol sequences up until this point, but now is taking much longer than expected to complete some step in the protocol. This can also happen because the SD card is now missing (perhaps because the user pulled it out in the middle of an I/O operation).
  • E_NOT_ACCEPTED (-4) — The SD card seems to be operating OK, but for some reason a write operation was rejected by the SD card (e.g., perhaps due to incorrect CRC or insufficient privileges).

Terminal Output: Cursor Management

term_cursor_on (8)

term_cursor_on()

This procedure decrements an internal counter. If the counter remains non-zero, no further action is taken. Otherwise, the cursor will be rendered again. See also term_cursor_off.

Note. After booting into software loaded from external storage, the cursor will be off by default. Your software must make a call to term_cursor_on to render the cursor again.

term_cursor_off (16)

term_cursor_off()

This procedure turns the cursor off if it's visible, and increments a counter. As long as the counter is non-zero, the cursor will remain off.

For every call to term_cursor_off, there must be a corresponding call to term_cursor_on for the cursor to be visible again.

term_cursor_swap (24)

term_cursor_swap(uint8_t *px, uint8_t *py)
                 A0           A1

This procedure exchanges the requested cursor position with the current cursor position.

On entry, px is a pointer to a byte holding the desired X coordinate, and py is a pointer to a byte holding the desired Y coordinate, respectively, of the new cursor position. If either *px or *py are beyond the valid dimensions of the screen, they will be clipped to the largest value possible.

On return, *px will contain the previous cursor X coordinate, and *py will contain the previous cursor Y coordinate.

This function can be used in three ways.

To recover the dimensions of the screen, you can attempt to move the cursor to the largest expressible coordinate:

uint8_t x = 255, y = 255;
term_cursor_swap(&x, &y);
term_cursor_swap(&x, &y);  // Restores previous position
printf(
    "The screen has %d rows and %d columns\n",
    y + 1,
    x + 1
);

To move the cursor to a new position, simply ignore the results:

uint8_t x = 40, y = 12;
term_cursor_swap(&x, &y);

To query the cursor's current position, you must restore the cursor's previous position. However, make sure you capture the results of the first call to term_cursor_swap:

uint8_t x, y;
term_cursor_swap(&x, &y);  // Grabs current position.
where.x = x;
where.y = y;
term_cursor_swap(&x, &y);  // Restores previous position.

Terminal Output: Displaying Text

term_out_clear (32)

term_out_clear()

Clears the screen, and homes the cursor to the upper, left-hand corner.

Note. You must turn the cursor off prior to clearing the screen. Clearing the screen will erase the cursor without updating BIOS cursor state. The next time the cursor is moved, turned off, or a character is printed, it will end up corrupting the display by leaving a stray inverse-video character cell (assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

term_cursor_off();
term_out_clear();
term_cursor_on();

term_out_chr (40)

term_out_chr(char ch)
             A0

Prints the provided character ch to the screen.

Some ASCII control values have certain interpretations which do not result in a graphic character being printed.

Character Code Result
$07 The BEL character; since the Kestrel-2DX has no audio facilities, it is ignored.
$08 Moves cursor to the left one character. Does not erase the character underneath the cursor, nor does it shift characters to the right.
$09 Advance cursor to the next tab-stop.
$0A Without altering the cursor's horizontal position, advance to the next line on the screen. Scroll if necessary.
$0B Vertical tab; ignored.
$0C Clears the screen.
$0D Without advancing the cursor to the next line, return the cursor to the far left-hand edge of the screen.

Tab-stops are located every eight characters on the screen.

All other characters are treated as graphic characters for printing. Note. Printing characters in the set {0..6, 14..31} is not officially supported, for these characters are intended for terminal control purposes. Future versions of the Kestrel-2DX BIOS may interpret more control characters in the future.

Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

term_cursor_off();
term_out_chr(ch);
term_cursor_on();

term_out_buf (48)

term_out_buf(char *buf, size_t length)
             A0         A1

Outputs a fixed-sized buffer to the screen. Bytes within the buffer are interpreted as 7-bit ASCII with an extended character set for character codes {128..255}.

Character codes {7..13} are treated as control characters. See term_out_chr for more details.

The buffer pointed to by buf may contain NUL characters. These characters are rendered like any other character. No more than length bytes will be printed.

Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

buf = "Text goes here...";
// ...
term_cursor_off();
term_out_buf(buf, strlen(buf));
term_cursor_on();

term_out_str (56)

term_out_str(char *buf)
             A0

Outputs a NUL-terminated string pointed to by buf to the screen. Character codes {6..13} are treated as control characters. See term_out_chr for more details. Character code 0 is considered NUL.

Note. You must turn the cursor off prior to printing any characters. The first character thus output will overwrite the cell containing the cursor. If/when the cursor is moved, or subsequently turned off for any other reason, it will end up corrupting the display by leaving a stray inverse-video character (assuming the BIOS implements the cursor as a solid block).

The proper sequence for emitting a character is:

buf = "Text goes here...";
// ...
term_cursor_off();
term_out_str(buf);
term_cursor_on();

Terminal Input: Keyboard

term_in_pollraw (64)

term_in_pollraw(uint16_t *praw, int *valid)
                A0              A1

This procedure polls the keyboard hardware for a raw scancode. If a scancode is available, it is assigned to *praw and *valid is set to a non-zero value. Otherwise, if no data is available yet, *valid is set to zero, and the contents of *praw is undefined.

praw is a pointer to a 16-bit half-word, which will hold the scancode if data are available. valid is a pointer to a 64-bit dword which will indicate if the value in praw is safe to use.

Scancodes are basically PS/2 scan codes, but slightly modified, as follows:

15 14 .. 9 8 7 .. 0
R 0 X scan code

where R is set if the scancode is a key release event. That is, when pressing a key, the scancode generated will have R clear; meanwhile, when releasing that same key, R will be set.

X is set if the key is an extended key code. One of the characteristics of the PS/2 keyboard encoding is that some keys share the same scancode, for backward compatibility with older 16-bit DOS applications running on IBM PC/XT or PC/AT computers. For example, with NUMLOCK turned off, pressing 4 on the numeric keypad generates the same scancode as pressing the cursor-left key, since on earlier keyboards, the numeric keypad doubled as the cursor movement keypad, depending on the state of the NUMLOCK key. Thus, the X bit allows interested applications the opportunity to distinguish between "traditional" and "extended" keys serving the same intended purpose.

The scan code, of course, corresponds to the PS/2 scan code assigned to the particular key in question.

term_in_rawtoascii (72)

term_in_rawtoascii(uint16_t raw, char *pascii, int *pvalid)
                   A0            A1            A2

This procedure attempts to convert the raw keyboard scancode as returned by term_in_pollraw, into a corresponding ASCII codepoint. Unfortunately, not all scancodes correspond to ASCII codepoints. Thus, this procedure may fail.

raw is the raw scancode as returned by term_in_pollraw. pascii is a pointer to a byte which will hold the resulting character if the translation is successful. pvalid is a pointer to a dword which will indicate if the translation succeeded or not.

If *pvalid is non-zero, the translation is successful, and *pascii will contain a valid ASCII codepoint. Otherwise, *pascii will be undefined, and its value must remain untrusted.

Note. Currently, *pascii is set to zero on entry to this function. However, do not depend on this behavior; this is simply an artifact of how it's currently written, and is not intended to be a formal part of its behavior.

term_in_getmeta (80)

uint8_t term_in_getmeta()
A0

Returns the current state of the CTRL and SHIFT flags. See also term_in_setshift and term_in_setctrl for details on how these flags work.

The resulting byte is a bitmask:

7 6 5 4 3 2 1 0
0 0 0 0 0 0 C S

where S represents the current state of the SHIFT flag, and C represents the current state of the CTRL flag.

All other bits are explicitly reserved for future use, and must be ignored by the caller for upward compatibility.

term_in_setshift (88)

term_in_setshift(int state)
                 A0

If state is non-zero, assert the SHIFT flag. This causes all future ASCII graphic characters submitted to term_in_rawtoascii to be treated as uppercase or, in the case of numerals, punctuation characters instead.

If state is zero, restore normal, lowercase graphic character handling semantics.

term_in_setctrl (96)

term_in_setctrl(int state)
                A0

If state is non-zero, assert the CTRL flag. This causes all future ASCII graphic characters submitted to term_in_rawtoascii to be treated as control characters instead.

If state is zero, restore normal graphic character handling semantics.

NOTE. The CTRL flag overrules the SHIFT flag. Thus, there is no difference between CTRL-A and ** CTRL-SHIFT-A; both will result in a character code of $01.

External Storage

sdmmc_writeblock (104)

err = sdcard_writeblock(block, buffer)
A0                      A0     A1

Copies a 512-byte block of data from the buffer provided in buffer to the storage sector named in block.

sdmmc_readblock (112)

err = sdcard_readblock(block, buffer)
A0                     A0     A1

Copies a 512-byte block of data from the sector number provided in block, to the buffer supplied in buffer. The caller is responsible for ensuring that buffer is big enough to hold at least 512 bytes.

sdmmc_idle (120)

err = sdmmc_idle()
A0

Performs an SD protocol initialization and idling sequence for the card, if any card exists in the slot. If, for any reason, a problem were to occur during this procedure, an appropriate error result is returned. It is vitally important that no I/O to the SD card be performed if an error is returned from this procedure. Doing so may result in abnormal system behavior and/or data loss, both in memory and on the SD card itself.

sdmmc_is_present (128)

flag = sdmmc_is_present()
 A0

Returns non-zero if an SD card is inserted into the slot; 0 otherwise.

spi_select (136)

err = spi_select(device)
A0                 A0

Addresses a specific SPI device for I/O. Currently, only device 0 is supported (the SD card slot). Devices 1..3 are accepted, but do not address any specific hardware in the stock Kestrel-2DX configuration.

Returns E_OK if given a valid device ID; otherwise, E_UNIT is returned.

spi_deselect (144)

spi_deselect()

Deselects all SPI devices.

Rationale

Why Scan for BiosInterface?

The RISC-V JAL instruction only covers a 2MB range of addresses relative to the JAL instruction itself. A future Kestrel design may well place RAM resources further than 2MB away from where the BIOS ROM sits. Thus, software cannot reliably use the JAL instruction to invoke BIOS services directly. It can, however, load a base address into a temporary register, typically t0, and invoke services through that using JALR.

Why Not Use ECALL?

When it was first written, the BIOS consisted of two parts: a raw assembly bootstrap routine, and the bulk of the functionality written in C. No means currently exists of informing the assembler where the C component's _start address resides. Further, I have had, and continue to have, great difficulty convincing GNU ld to place the .text segment where I desire it. If I had control over this, this whole problem could have been avoided. Alas, it refuses to place .text ahead of .data, and so here we are. Thus, just like application software loaded from external storage, it must scan memory to find this entry point. Thus, one must wonder what value to set the mtvec register to handle machine-mode traps with? While this is a solvable problem, at the time, I considered this approach easier to get working sooner.

One more note on this: using ECALL necessarily means that mtvec would need very careful handling if a loaded operating system wanted to use ECALL itself to handle system calls. It would, in effect, circumvent BIOS all-together, leaving the BIOS with no clear way of being invoked, except through a process of chaining. Careful coordination between BIOS and the operating system would be needed to ensure BIOS and the OS could evolve independently of each other.

Why Start Scan at Address $10000?

The BIOS code necessarily incorporates this constant as a value within its ROM. If scanning starting at address 0, then application programs will find the instance in ROM, not in RAM. The ROM-resident table of pointers to BIOS functions have not been properly relocated, and so calling them directly will only crash your software.

Starting the scan at address $10000 ensures that scanning starts at the beginning of RAM, where the BIOS will have copied and properly relocated all the pointers within the BiosInterface table.